<template>
    <div
        class="tetris"
        ref="tetrisEl"
        :style="{
            width: `${data.size * 10}px`,
            height: `${data.size * 20}px`,
        }"
        :class="{
            winAnimate: tetrisAnimateStatus.player == 'win',
            gameoverAnimate: tetrisAnimateStatus.player == 'gameover',
            tetrisEnter: tetrisAnimateStatus.player == 'enter',
            game: tetrisAnimateStatus.player == 'game',
            showWarning: data.showWarning,
        }"
        v-show="data.status != 0"
    >
        <!-- 场地 -->
        <div
            class="item courtItem flex-[10%] flex items-center justify-center"
            v-for="(item, index) in data.tetrisList"
            :key="index"
            :style="{
                height: `${data.size}px`,
                top: `-${data.size * 2}px`,
                backgroundColor: item.color || 'transparent',
            }"
            :class="{
                notRight: item.x == 9,
                notBottom: item.y == 19,
                top: item.y < 0,
                hasColor: item.color,
                shadow: item.shadow,
                animate_grind: item.animate == 'grind',
                showWarning: item.y < 0 && data.showWarning,
            }"
        >
            <div class="lightBorder" v-if="item.lightBorder"></div>
        </div>

        <!-- 当前块 -->
        <div
            class="item targetItem"
            v-for="(item, index) in data.target"
            :key="index"
            :style="{
                width: `${data.size}px`,
                height: `${data.size}px`,
                backgroundColor: item.color || 'transparent',
                transform: `translateX(${data.size * item.x - 1}px) translateY(${data.size * item.y - 1}px)`,
                boxShadow: `0 0 ${data.size * 2}px ${item.color}`,
            }"
            :class="{
                notRight: item.x == 9,
                notBottom: item.y == 19,
                hasColor: item.color,
                top: item.y < 0,
            }"
        ></div>

        <!-- hold -->
        <div v-if="data.status != 0" class="hold flex items-center justify-center">
            <viewBlock :minoType="data.bagList.targetHold" :size="data.size" />
        </div>

        <!-- 预览窗口 -->
        <div v-if="data.status != 0" class="viewList">
            <div v-for="(i, index) in nextBagList" :key="index" class="viewListItem flex items-center justify-center">
                <viewBlock :minoType="i" :size="index == 0 ? data.size * 1.2 : data.size" />
            </div>
        </div>

        <!-- 受击条 -->
        <div class="atkLine">
            <div
                class="line"
                :style="{
                    minHeight: `${atkLineHeight}`,
                }"
            ></div>

            <div
                v-if="tetrisInfo.createRubbishInfo.createTimeMode == 'delay'"
                class="loadEndLine"
                :style="{
                    minHeight: `${loadEndArkLineHeight}`,
                }"
            ></div>
        </div>

        <!-- 场地边框 -->
        <div class="atkBorder">
            <div class="left" :style="{ minHeight: arkBorderHeight }"></div>
            <div class="right" :style="{ minHeight: arkBorderHeight }"></div>
            <div class="bottom" v-show="tetrisInfo.createRubbishInfo.createTimeMode == 'delay' ? loadEndArkLineHeight != '0%' : atkLineHeight != '0%'"></div>
        </div>

        <!-- 游戏局内信息 -->
        <div v-if="data.status != 0" class="tetrisInfo flex flex-col items-end">
            <div v-if="tetrisInfo.confrontModeInfo.enable" class="title">ENEMY HP</div>
            <div v-if="tetrisInfo.confrontModeInfo.enable" class="content">{{ tetrisInfo.confrontModeInfo.hp }} / {{ tetrisInfo.confrontModeInfo.maxHp }}</div>

            <template v-if="tetrisInfo.time">
                <div class="title">LINE</div>
                <div v-if="tetrisInfo.targetLine" class="content">{{ tetrisInfo.line }} / {{ tetrisInfo.targetLine }}</div>
                <div v-else class="content">
                    {{ tetrisInfo.line }}
                </div>

                <div class="title">TIME</div>
                <div class="content">
                    {{ rafTimer.timeFormat }}
                </div>
            </template>
        </div>

        <!-- ren 连击文字 -->
        <div v-if="data.ren > 0" class="renInfo">
            <span class="num">{{ data.ren }}</span>
            COMBO
        </div>

        <!-- tetris canvas -->
        <canvas ref="canvasEl" class="w-full h-full absolute tetrisCanvas"></canvas>

        <!-- 特殊消行文字 -->
        <div class="skillText" v-if="data.skillText">
            {{ data.skillText }}
        </div>

        <!-- 锁定延迟条 -->
        <div
            class="lockDelayLine"
            v-if="tetrisInfo.lockDelay && data.lockDelayTimer.progress"
            :style="{
                minWidth: data.lockDelayTimer.progress + '%',
            }"
        ></div>

        <!-- 短期爆发显示 -->
        <div class="linksText" v-if="data.atkLinksCount">{{ data.atkLinksCount }} LINKS</div>
    </div>

    <div
        v-if="data.type == 'solo'"
        class="opponent tetris"
        :style="{
            width: `${data.size * 10}px`,
            height: `${data.size * 20}px`,
            marginLeft: '10vmin',
        }"
        :class="{
            winAnimate: tetrisAnimateStatus.opponent == 'win',
            gameoverAnimate: tetrisAnimateStatus.opponent == 'gameover',
            tetrisEnter: tetrisAnimateStatus.opponent == 'enter',
        }"
        v-show="data.status != 0"
    >
        <!-- 当前块 -->
        <div
            class="item targetItem"
            v-for="(item, index) in data.opponentTarget"
            :key="index"
            :style="{
                width: `${data.size}px`,
                height: `${data.size}px`,
                backgroundColor: item.color || 'transparent',
                transform: `translateX(${data.size * item.x - 1}px) translateY(${data.size * item.y - 1}px)`,
                boxShadow: `0 0 ${data.size * 2}px ${item.color}`,
            }"
            :class="{
                notRight: item.x == 9,
                notBottom: item.y == 19,
                top: item.y < 0,
                hasColor: item.color,
            }"
        ></div>

        <!-- 场地 -->
        <div
            class="item flex-[10%] flex items-center justify-center"
            v-for="(item, index) in data.opponentTetrisList"
            :key="index"
            :style="{
                height: `${data.size}px`,
                top: `-${data.size * 2}px`,
                backgroundColor: item.color || 'transparent',
            }"
            :class="{
                notRight: item.x == 9,
                notBottom: item.y == 19,
                top: item.y < 0,
                hasColor: item.color,
                shadow: item.shadow,
                animate_grind: item.animate == 'grind',
            }"
        >
            <div class="lightBorder" v-if="item.lightBorder"></div>
        </div>

        <!-- hold -->
        <div class="hold flex items-center justify-center">
            <viewBlock :minoType="data.opponentHold" :size="data.size" />
        </div>

        <!-- 预览窗口 -->
        <div v-if="data.status != 0" class="viewList">
            <div v-for="(i, index) in data.opponentNextList" :key="index" class="viewListItem flex items-center justify-center">
                <viewBlock :minoType="i" :size="data.size" />
            </div>
        </div>

        <!-- 受击条 -->
        <div class="atkLine">
            <div
                class="line"
                :style="{
                    minHeight: `${data.opponentAtk}`,
                }"
            ></div>
        </div>
    </div>

    <!-- 预备 -->
    <div class="tetrisReady" v-if="data.status == 2">
        <span class="readyAnimate" v-if="data.readyCount == 0">GO</span>
        <span class="readyAnimate" v-else-if="data.readyCount <= 3">{{ data.readyCount }}</span>
    </div>

    <!-- 游戏标题 -->
    <div class="tetrisReadyTitle flex items-center justify-center" v-if="data.status == 2 && data.readyCount > 3">
        <div class="inset"></div>
        <div class="top"></div>
        <div class="bottom"></div>
        <span class="text">{{ getDisplayName }}</span>
    </div>

    <!-- 头部 显示对战信息 -->
    <div class="tetrisTop flex items-center" v-if="data.type == 'solo'" v-show="data.status == 1">
        <div class="title flex items-center justify-center">QUICK MATCH</div>
        <div class="name flex items-center justify-center">
            {{ app.soloInfo.me.name }}
        </div>
        <div class="vs flex items-center justify-center">VS</div>
        <div class="name opponentName flex items-center justify-center">
            {{ app.soloInfo.opponent.name }}
        </div>
    </div>

    <!-- <div class="flex h-20 gap-10 font-black text-gray-300 text-6xl items-center" v-if="isMobile()">
    <div class="" @touchstart="left"><el-icon><Back /></el-icon></div>
    <div class="button" @touchstart="right"><el-icon><Right /></el-icon></div>
    <div class="button" @touchstart="spin"><el-icon><Refresh /></el-icon></div>
    <div class="button" @touchstart="hardDown"><el-icon><Download /></el-icon></div>
  </div> -->

    <!-- 移动端 虚拟按钮 -->
    <div class="virtualButtonBox" v-if="isMobile() && data.status == 1">
        <virtualButton :isEdit="false" />
    </div>

    <div v-if="data.type == 'single' && data.status == 1" class="absolute top-6 left-6 font-black text-gray-300 text-3xl">
        <div class="pause" @click="actionPause">
            <el-icon>
                <VideoPause />
            </el-icon>
        </div>
    </div>

    <pauseDialog :type="data.type" ref="pauseDialogEl" @hide="handleHidePause" @restart="again" @showSetting="handleShowSetting" @exit="exit" />

    <settingDialog ref="SettingDialog" @hide="settingHide" />

    <gameResultPopup ref="gameResultPopupEl" @again="again" />

    <!-- 添加歌曲名显示组件 -->
    <Transition enter-active-class="animate__animated animate__fadeInUp" leave-active-class="animate__animated animate__fadeOutDown">
        <div v-if="showSongName" class="songName">BGM - {{ songName }}</div>
    </Transition>
</template>

<script setup lang="ts">
import mino from "@/data/mino.json"
import { reactive, ref, computed, onMounted, watch, getCurrentInstance, ComponentInternalInstance, defineEmits, nextTick } from "vue"
import { onActivated, onDeactivated } from "vue"
import { useWindowSize, watchDebounced, useWindowFocus, TransitionPresets, useTransition } from "@vueuse/core"
import { Bag, getAfterSpinBlock, checkClearLine, isMobile, arrayRange, RafTimer } from "@/utils/utils"
import { getTetrisInfo, getClearLineInfo, createRubbishLine, autoCreateRubbishLine, initTetrisCourt, AtkLoader, LockDelayTimer } from "@/utils/game"
import { getKeyBoard, getTetrisConfig, getUserConfig } from "@/utils/config"
import { Back, Right, Refresh, Download, VideoPause } from "@element-plus/icons-vue"
import settingDialog from "@/components/settingDialog.vue"
import viewBlock from "@/components/viewBlock.vue"
import pauseDialog from "@/components/pauseDialog.vue"
import gameResultPopup from "@/components/gameResultPopup.vue"
import virtualButton from "@/components/virtualButton.vue"
import firework from "@/utils/firework.js"
import dayjs from "@/utils/dayjs.ts"
import "animate.css"
import { useRoute, useRouter } from "vue-router"
const route = useRoute()
const router = useRouter()
import useAppStore from "@/pinia"
const app = useAppStore() // pinia实例
import { i18n } from "@/utils/i18n"
import { gsap } from "gsap"
import { Physics2DPlugin } from "gsap/Physics2DPlugin"
gsap.registerPlugin(Physics2DPlugin)

const canvasEl = ref()
const tetrisEl = ref()
const SettingDialog = ref()
const pauseDialogEl = ref()
const gameResultPopupEl = ref()
let gsapGameOverCtx = null as any

// 添加歌曲名显示相关的响应式变量
const songName = ref("")
const showSongName = ref(false)

// tetris config 通用属性
const data = reactive({
    // 路由激活时更新
    type: "single", // 默认单人游戏
    mode: "",
    firstGame: true, // 是否为首局游戏

    // 由配置导入 不直接修改
    das: 100, // 距离快速移动的时间差
    arr: 20, // 快速移动阶段的速度
    sortArr: 20, // 软降速度
    lang: "en", // 语言
    autoPause: true, // 窗口失去焦点 自动暂停

    // 基本属性
    size: 0,
    status: 0, // 状态 -1: 暂停 | 0: 未开始 | 1: 进行中 | 2: 准备开始
    isGameOver: false, // 是否已游戏结束(status仍为1)
    tetrisList: [] as any[], // 场地
    fixed: ref([]) as any, // 固定块
    target: [] as any[], // 当前块
    animateIndex: ref([]) as any, // 当前显示动画的固定块的index
    deg: 0, // 当前旋转角度
    btb: false, // b2b
    ren: -1, // 连击
    skillText: "", // 特殊消行
    lastAction: "", // 最后一次操作类型 用于判断tspin
    showWarning: false, // 显示tetris高层警告

    readyCount: 6, // 准备计数
    isCanFreeFall: true, // 是否可以自由下落
    bagList: new Bag(), // 方块袋
    atkLoader: new AtkLoader(), // 攻击加载器
    lockDelayTimer: new LockDelayTimer(), // tetris lock delay timer 锁定延迟计时器

    opponentTarget: [] as any[], // 对手场地(solo)
    opponentTetrisList: [] as any[], // 对手场地(solo)
    opponentHold: "", // 对手hold(solo)
    opponentNextList: [], // 对手nextList(solo)
    opponentAtk: "0%", // 对手攻击条(solo)

    atkLinksCount: 0, // 当前短期爆发的攻击数
    showAtkLinksCount: false, // 是否显示短期爆发攻击数统计 为false时 当atkLinksCount大于0 则会在2秒后自动重置为0
}) as any

const tetrisTotal = reactive({
    single: 0,
    double: 0,
    triple: 0,
    tetris: 0,
    tspin: 0,
    tspinSingle: 0,
    tspinDouble: 0,
    tspinTriple: 0,
    btb: 0,
    btbTetris: 0,
    btbTspinSingle: 0,
    btbTspinDouble: 0,
    btbTspinTriple: 0,
    allClear: 0, // 全消
    maxCombo: 0,
    clearLine: 0, // 累计消行
    clearRubbishLine: 0, // 累计消除垃圾行数
    atkTotal: 0, // 累计攻击
    getAtkTotal: 0, // 累计受到攻击
    damageTotal: 0, // 累计垃圾行
    score: 0,
})

// tetris info 游戏左侧展示的局内信息
const tetrisInfo = reactive({
    defaultTetrisCourt: "", // 默认地图
    defaultTetrisCourtLevel: 1, // 默认地图等级
    bagRule: "7Bag", // 方块袋规则
    showNextNumber: 5, // 预览方块个数
    canHold: true,
    gravity: 0, // 重力 自由下落的速度 默认为1 最小为0(无重力 不自由下落) 最高为20 越大自由下落越快
    lockDelay: -1, // 锁定延迟 -1为不锁定 1000为1000ms后锁定
    time: 0, // 游戏逻辑计时 不直接展示 毫秒
    line: 0, // 累计消行
    targetLine: 0, // 目标行数
    createRubbishInfo: {
        // 创建垃圾行配置
        enable: false, // 是否开启
        frequency: 1000, // 频率 每1000ms创建一次
        count: 4, // 每次创建数量
        createTimeMode: "now", // 垃圾行生成时机 默认为立即生成所有垃圾行
        mode: "default", // 垃圾行生成规则
        oneRubbishX: 0, // mode为one时 创建垃圾行的开口的x坐标
        delayInfo: {
            // 延迟生成配置 createTimeMode为delay时生效
            delay: 10000, // 延迟时间
            maxCount: 8, // 每次生成最大数量
        },
    },
    autoCreateRubbishInfo: {
        // 自动生成垃圾行配置(每隔一段时间自动生成垃圾行 且不会经过atkLoader直接生成)
        enable: false, // 是否开启自动创建垃圾行
        count: 1, // 每次创建行数
        airSpan: [1, 1], // 垃圾行开口数量的范围 默认开口1格
        delay: 5000, // 间隔时间 必须以1000ms为最小单位
    },
    confrontModeInfo: {
        enable: false, // 当前为对抗模式
        maxHp: 20, // 最大生命
        hpRecoverSpeed: 4000, // 每4000ms 恢复1点生命
    },
    gameMode: {
        name: "",
        id: "",
        desc: "",
        eName: "",
    },
    start: "",
    end: "",
    lastClearLineRafTime: 0, // 最后一次消行的对局时间
    hasLoss: true, // 是否存在游戏失败 关联结算界面
    showTotalKey: ["apm", "lpm", "apl", "vs", "time", "mode", "line", "targetLine"],
    best: "time", // 最佳记录判断依据
}) as any

// dg-lab相关
const tetrisDgInfo = reactive({}) as any

// tetris animate
const tetrisAnimateStatus = reactive({
    player: "enter",
    opponent: "enter",
})

// tetris key 键盘按下状态
const keyBoard = reactive({
    left: false,
    right: false,
    sortDown: false, // 软降
    hardDown: false, // 硬降
    spin: false, // 旋转
    spinBack: false, // 逆时针旋转
    hold: false,
})

// tetris 场地震动动画 动画属性
const tetrisShakeDuration = 40
const tetrisTranslateY = ref(0)
const tetrisScale = ref(1)
const tetrisOpacity = ref(1)
const tetrisTranslateYOutPut = useTransition(tetrisTranslateY, {
    duration: tetrisShakeDuration,
    transition: TransitionPresets.easeInOutCubic,
})
const tetrisScaleOutPut = useTransition(tetrisScale, {
    duration: tetrisShakeDuration,
    transition: TransitionPresets.easeInOutCubic,
})
const tetrisOpacityOutPut = useTransition(tetrisOpacity, {
    duration: tetrisShakeDuration,
    transition: TransitionPresets.easeInOutCubic,
})
const tetrisPlayerTransform = computed(() => {
    return `scale(${tetrisScaleOutPut.value}) translateY(${tetrisTranslateYOutPut.value}%)`
})
// 复位
let resetTimer = null as any
const resetTetrisPlayerTransform = () => {
    if (resetTimer) return
    resetTimer = setTimeout(() => {
        tetrisTranslateY.value = 0
        tetrisScale.value = 1
        tetrisOpacity.value = 1
        clearTimeout(resetTimer)
        resetTimer = null
    }, tetrisShakeDuration)
}

// tetris DAS Timer dns计时器
let dasTimer = null as any
// tetris sort down Timer 软降计时器
let sortDownTimer = null as any
// tetris raf Timer 帧动画计时器
let rafTimer = new RafTimer()
// dg-leb 恢复强度定时器
let dgReloadTimer = null as any

// ------------------------- 计算属性 ---------------------------------

// tetris bag list
const nextBagList = computed(() => data.bagList.nextMinoList)

const atkLineHeight = computed(() => {
    const total = tetrisInfo.createRubbishInfo.createTimeMode == "now" ? data.atkLoader.atkTotal : data.atkLoader.loadingAtkTotal
    return total > 20 ? "100%" : total * 5 + "%"
})

const loadEndArkLineHeight = computed(() => {
    const total = data.atkLoader.atkTotal
    return total > 20 ? "100%" : total * 5 + "%"
})

const arkBorderHeight = computed(() => {
    return tetrisInfo.createRubbishInfo.createTimeMode == "delay" ? loadEndArkLineHeight.value : atkLineHeight.value
})

const getDisplayName = computed(() => {
    return i18n.global.locale === "zn" ? tetrisInfo.gameMode.name : tetrisInfo.gameMode.eName
})

// ------------------------- 计算属性 结束 ---------------------------------

// ------------------------- 初始化方法 ---------------------------------

const { height: WindowHeight } = useWindowSize()
const isWindowFocus = useWindowFocus() // 窗口是否聚焦
const windowResize = () => {
    initTetrisSize()
}
const isWindowFocusChange = () => {
    if (data.autoPause && data.status == 1 && !isWindowFocus.value) {
        actionPause() // 游戏进行中 窗口失去聚焦 暂停
    }
}
// 防抖监听WindowHeight变化
watchDebounced(WindowHeight, windowResize, { debounce: 200 })
// 防抖监听窗口是否聚焦
watchDebounced(isWindowFocus, isWindowFocusChange, { debounce: 200 })

const initTetrisSize = () => {
    let number = 0.8
    if (data.type == "solo") number = 0.7 // 同时展示两个场地 比例缩小
    const height = Math.floor(WindowHeight.value * number)
    const size = Math.floor(height / 20)
    data.size = size
    console.log("场地大小更正 initTetrisSize", data)
}

const initTetrisConfig = () => {
    const tetrisConfig = getTetrisConfig()
    Object.keys(tetrisConfig).map((key) => {
        data[key] = tetrisConfig[key]
    })
    console.log("tetris配置加载", tetrisConfig)
}

const initUserConfig = () => {
    const userConfig = getUserConfig()
    Object.keys(userConfig).map((key) => {
        data[key] = userConfig[key]
    })
    console.log("用户配置加载", userConfig)
}

const initTetrisData = () => {
    data.status = 0
    data.isGameOver = false
    data.target = []
    data.fixed = ref([])
    data.deg = 0
    data.btb = false
    data.ren = -1
    data.lastAction = ""
    data.showWarning = false
    data.animateIndex = ref([])
    data.readyCount = 6
    data.isCanFreeFall = true
    data.skillText = ""
    keyBoard.left = false
    keyBoard.right = false
    keyBoard.sortDown = false
    keyBoard.hardDown = false
    keyBoard.spin = false
    keyBoard.spinBack = false
    keyBoard.hold = false

    data.opponentTarget = []
    data.opponentHold = ""
    data.opponentNextList = []
    data.opponentAtk = "0%"

    data.atkLinksCount = 0
    data.showAtkLinksCount = false
    console.log("初始化data")
}

const initTetrisInfo = () => {
    const result = getTetrisInfo(data.mode)
    Object.keys(result).map((key) => {
        tetrisInfo[key] = result[key]
    })
    tetrisInfo.start = ""
    tetrisInfo.end = ""
    console.log("初始化局内信息", tetrisInfo)
}

const initTetrisTotal = () => {
    tetrisTotal.single = 0
    tetrisTotal.double = 0
    tetrisTotal.triple = 0
    tetrisTotal.tetris = 0
    tetrisTotal.tspin = 0
    tetrisTotal.tspinSingle = 0
    tetrisTotal.tspinDouble = 0
    tetrisTotal.tspinTriple = 0
    tetrisTotal.btb = 0
    tetrisTotal.btbTetris = 0
    tetrisTotal.btbTspinSingle = 0
    tetrisTotal.btbTspinDouble = 0
    tetrisTotal.btbTspinTriple = 0
    tetrisTotal.allClear = 0
    tetrisTotal.maxCombo = 0
    tetrisTotal.clearLine = 0 // 累计消行
    ;(tetrisTotal.clearRubbishLine = 0), // 累计消除垃圾行数
        (tetrisTotal.atkTotal = 0) // 累计攻击
    tetrisTotal.getAtkTotal = 0 // 累计受到攻击
    tetrisTotal.damageTotal = 0 // 累计垃圾行
    tetrisTotal.score = 0
    console.log("初始化统计信息", tetrisTotal)
}

const initList = () => {
    let list = []
    for (let y = -2; y < 20; y++) {
        for (let x = 0; x < 10; x++) {
            list.push({
                x,
                y,
                color: "",
                type: "",
            })
        }
    }
    list = list.map((i, index) => {
        i.index = index
        return i
    })
    data.tetrisList = list
    if (data.type == "solo") {
        // 单人对战 初始化对手场地
        data.opponentTetrisList = JSON.parse(JSON.stringify(list))
    }
    console.log("初始化场地")
}

const initTetrisAnimateStatus = () => {
    tetrisAnimateStatus.player = "enter"
    tetrisAnimateStatus.opponent = "enter"
    setTimeout(() => {
        // 进场动画结束 1.2秒
        tetrisAnimateStatus.player = "game"
        tetrisAnimateStatus.opponent = "game"
    }, 1200)
    tetrisTranslateY.value = 0
    tetrisScale.value = 1
}

// 游戏结束后触发 统计
const gameResultTotal = () => {
    const result = {} as any
    Object.keys(tetrisInfo).forEach((key) => {
        result[key] = tetrisInfo[key]
    })
    result.total = JSON.parse(JSON.stringify(tetrisTotal)) // 去响应式
    result.timeFormat = rafTimer.timeFormat
    result.rafTime = rafTimer.rafTime
    result.type = data.type
    return result
}

// ------------------------- 初始化方法 结束 ---------------------------------

// 生成新的方块 在初始位置生成
const createNewBlock = () => {
    if (data.isGameOver) return // 已进入游戏结束判断
    data.target = []
    data.deg = 0
    const targetMino = data.bagList.targetMino
    const location = targetMino.location
    let target = location.map((i: any) => {
        return {
            type: targetMino.type,
            color: targetMino.color,
            x: i[0],
            y: i[1],
        }
    })
    if (data.type != "single") {
        // 非单人模式 推送当前方块袋与hold
        app.updatePlayerInfo({
            roomId: app.roomId,
            nextMinoList: data.bagList.nextMinoList,
            hold: data.bagList.targetHold,
        })
    }
    data.lastAction = "" // 重置当前块上次操作
    let isGameOver = target.some((i: any) => {
        // 方块生成后 会触发游戏结束
        return data.fixed.findIndex((j: any) => j.x == i.x && j.y == i.y) != -1
    })
    if (isGameOver) {
        if (data.mode != "zen") {
            // 禅模式下不会游戏结束 不做处理
            let flag = true
            while (flag) {
                // 尝试将新生成的方块生成到合理的位置 再结束游戏
                target = target.map((i: any) => {
                    i.y--
                    return i
                })
                flag = target.some((i: any) => {
                    return data.fixed.findIndex((j: any) => j.x == i.x && j.y == i.y) != -1
                })
                if (!flag) {
                    data.target = target
                    if (data.type != "single")
                        app.updateTargetBlock({
                            roomId: app.roomId,
                            list: target,
                        })
                }
            }
        }
        gameOver()
        return false
    } else {
        data.target = target
        if (data.type != "single")
            app.updateTargetBlock({
                roomId: app.roomId,
                list: target,
            })
        return true
    }
}

// 渲染 (先重置场地 再渲染固定块 会清空阴影 渲染固定块后需渲染阴影)
const renderList = () => {
    // 重置场地
    data.tetrisList = data.tetrisList.map((i: any) => {
        i.type = ""
        i.color = ""
        i.shadow = false
        return i
    })

    // 渲染固定块
    data.fixed.forEach((item: any) => {
        if (item.index || item.index == 0) {
            data.tetrisList[item.index].color = item.color
            data.tetrisList[item.index].type = item.type
        }
    })

    // 非单人模式 同步场地
    if (data.type != "single") {
        app.updateFixedList({
            roomId: app.roomId,
            list: data.fixed,
        })
    }

    data.showWarning = data.fixed.some((i: any) => i.y <= 1)
}

// 渲染对手场地 (单人模式不使用)
const updateOpponentFixedList = (e: any) => {
    // 重置场地
    data.opponentTetrisList = data.opponentTetrisList.map((i: any) => {
        i.type = ""
        i.color = ""
        i.shadow = false
        return i
    })

    // 渲染固定块
    e.list.forEach((item: any) => {
        if (item.index || item.index == 0) {
            data.opponentTetrisList[item.index].color = item.color
            data.opponentTetrisList[item.index].type = item.type
        }
    })
}

// 渲染当前块的阴影
const renderShadow = () => {
    if (data.status == 0) return
    let { target, isGameOver } = getAfterHardDownTarget()
    if (isGameOver) return
    data.tetrisList = data.tetrisList.map((i: any) => {
        const changeIndex = target.findIndex((item: any) => item.x == i.x && item.y == i.y)
        if (changeIndex > -1) {
            i.shadow = true
            i.color = target[0].color
        } else {
            if (i.shadow) i.color = ""
            i.shadow = false
        }
        return i
    })
}

// 将所有固定块转化为垃圾行 游戏结束
const renderGameOverList = () => {
    updateFixedCourtIndex()
    renderList()
    let rubbishColor = mino.find((i: any) => i.type == "rubbish").color
    const result = data.tetrisList.map((i: any) => {
        if (i.color && !i.shadow) {
            i.color = rubbishColor
            i.type = "rubbish"
        }
        return i
    })
    data.tetrisList = result
}

// 将所有固定块转化为特殊方块(胜利) 游戏结束
const renderVictoryList = () => {
    updateFixedCourtIndex()
    renderList()
    let rubbishColor = mino.find((i: any) => i.type == "rubbish").color
    const result = data.tetrisList.map((i: any) => {
        if (i.color && !i.shadow) {
            i.color = ""
            i.type = ""
            i.lightBorder = true
        }
        return i
    })
    data.tetrisList = result
}

// 自由下落
const down = (sort = false) => {
    // sort = true时为软降
    if (!data.isCanFreeFall && !sort) {
        if (tetrisInfo.lockDelay > 0 && data.lockDelayTimer.status == 2) targetFixed()
        return
    }
    let hasDown = true
    // 继续下落则触底
    const isBottom = data.target.some((i: any) => {
        return i.y >= 19
    })
    // 继续下落则与固定块重合
    const isNearFixed = data.target.some((i: any) => {
        return data.fixed.some((item: any) => {
            return item.x == i.x && item.y == i.y + 1
        })
    })
    hasDown = !isBottom && !isNearFixed
    // 可以下落
    if (hasDown) {
        data.lastAction = "down"
        data.target = data.target.map((i: any) => {
            i.y++
            return i
        })
        if (data.type != "single")
            app.updateTargetBlock({
                roomId: app.roomId,
                list: data.target,
            })
    } else {
        if (tetrisInfo.lockDelay > 0) {
            // 存在锁定延迟
            if (data.lockDelayTimer.status == 0) data.lockDelayTimer.start() // 开启锁定延迟
            else if (data.lockDelayTimer.status == 1) return // 运行中
            else if (data.lockDelayTimer.status == 2) targetFixed() // 锁定延迟结束 触底直接锁定
        } else if (tetrisInfo.lockDelay == 0) targetFixed() // 触底直接锁定
    }
}

// 获取硬降后的当前块的位置
const getAfterHardDownTarget = () => {
    let isOk = true
    let flag = 0
    let target = data.target
    target = JSON.parse(JSON.stringify(target))
    let isGameOver = target.some((i: any) => {
        return data.fixed.some((item: any) => {
            return item.x == i.x && item.y == i.y
        })
    })
    if (isGameOver) {
        // 游戏结束 尝试将结束前的最后一个方块 固定在合理的位置上
        while (isOk && flag < 100) {
            flag++
            const isHasFixed = target.some((i: any) => {
                // 存在重叠
                return data.fixed.some((item: any) => {
                    return item.x == i.x && item.y == i.y
                })
            })
            isOk = isHasFixed
            if (isOk) {
                target = target.map((i: any) => {
                    // 总体向上一格
                    i.y--
                    return i
                })
            }
        }
    } else {
        while (isOk && flag < 100) {
            flag++
            const isBottom = target.some((i: any) => {
                return i.y >= 19
            })
            const isNearFixed = target.some((i: any) => {
                return data.fixed.some((item: any) => {
                    return item.x == i.x && item.y == i.y + 1
                })
            })
            isOk = !isBottom && !isNearFixed
            if (isOk) {
                target = target.map((i: any) => {
                    i.y++
                    return i
                })
            }
        }
    }
    return {
        target,
        isGameOver,
    }
}

// 硬降
const hardDown = () => {
    if (sortDownTimer) sortDownTimerStop() // 正在软降 停止软降
    let { target, isGameOver } = getAfterHardDownTarget()
    data.target = target
    // app.playSound('harddrop')
    targetFixed(isGameOver)
    if (isGameOver) gameOver()

    //   tetrisEl.value.classList.add('bouncing')
    //   setTimeout(() => tetrisEl.value.classList.remove('bouncing'), 100)
}

// 硬降后 显示动画
const showAfterHardDownAnimate = (target: any) => {
    const animateIndex = target.map((i: any) => {
        let index = data.tetrisList.findIndex((item: any) => item.x == i.x && item.y == i.y)
        return index
    })
    data.animateIndex.forEach((index: any) => {
        if (index > -1) data.tetrisList[index].animate = ""
    })
    data.animateIndex = animateIndex
    data.animateIndex.forEach((index: any) => {
        if (index > -1) data.tetrisList[index].animate = "grind"
        // const item = data.tetrisList[index]
        // firework.boom(data.size * item.x, data.size * item.y)
    })
}

// 当前块固定 同时创建新的块 并渲染
const targetFixed = (isGameOver = false) => {
    const targetBlock = [...data.target]
    data.isCanFreeFall = false
    data.fixed.push(...data.target) // 当前块转化为固定块
    const checkClearLineResult = checkClearLine(data.fixed, data.target, data.lastAction) // 消行检测
    clearLineCallback(checkClearLineResult)
    // 未消行 加载垃圾行
    if (!checkClearLineResult.hasClearLine) {
        if (data.atkLoader.atkTotal) {
            // 存在累计攻击 创建垃圾行
            console.log("存在累计攻击 创建垃圾行 data.atkLoader.atk", data.atkLoader.atk)
            const createRubbishLineResult = createRubbishLine(data.fixed, data.atkLoader.atk, tetrisInfo.createRubbishInfo)
            let createRubbishLineNumber = data.atkLoader.handleLoadAtk() // 消耗完所有已就绪垃圾行
            data.fixed = createRubbishLineResult.resultFixed // 创建垃圾行后的固定块
            updateFixedCourtIndex() // 更新场地index
            renderList() // 渲染
            // 更新统计
            tetrisTotal.damageTotal += Number(createRubbishLineNumber) // 累计接收到的垃圾行数量
        }
    }
    let save = true as any
    if (!isGameOver) {
        // 当前块固定后是否立即死亡
        data.target = [] // 当前块重置
        data.bagList.next() // 下一块
        save = createNewBlock() // 生成新的块
    }
    if (save) {
        // 生成新的方块后是否死亡
        renderList() // 渲染
        renderShadow() // 渲染阴影
        if (!checkClearLineResult.result) {
            // 未消行 显示动画
            showAfterHardDownAnimate(targetBlock)
        }
        if (tetrisInfo.lockDelay > 0) {
            // 存在锁定延迟
            data.lockDelayTimer.reset(true) // 重置锁定延迟定时器
        }
    }
    data.isCanFreeFall = true
}

// 消行回调
const clearLineCallback = (checkClearLineResult: any) => {
    const { result, hasTSpin, hasClearLine, hasClearRubbishLine, clearLineY } = checkClearLineResult
    let playAudioName = "down" // 未消行音效
    let skillText = ""
    let ren = -1 // 未消行 连击重置
    if (result) {
        // 消行
        data.fixed = result
        // console.log('消除的行', arrayRange(clearLineY-hasClearLine+1, clearLineY))
        arrayRange(clearLineY - hasClearLine + 1, clearLineY).forEach((lino: any) => arrayRange(0, 9).forEach((x: any) => firework.boom(data.size * x, data.size * lino)))
        ren = data.ren + 1 // 连击 + 1
        const clearLineInfo = getClearLineInfo({
            btb: data.btb,
            hasClearLine,
            hasClearRubbishLine,
            hasTSpin,
            ren,
            result,
            tetrisTotal: { ...tetrisTotal },
        })
        playAudioName = clearLineInfo.playAudioName // 消行音效
        data.btb = clearLineInfo.btb // btb激活
        skillText = clearLineInfo.skillText // 特殊消行
        tetrisInfo.line += hasClearLine // 累计行数增加
        if (clearLineInfo.atk) sendAtk(clearLineInfo.atk) // 攻击
        if (clearLineInfo.renAtk) {
            if (tetrisInfo.renLimitInfo.endTime) {
                // 当前已进入连击限制状态 连击伤害 - 1
                clearLineInfo.renAtk--
                if (clearLineInfo.renAtk) sendAtk(clearLineInfo.renAtk) // 攻击(连击造成)
            } else sendAtk(clearLineInfo.renAtk) // 攻击(连击造成)
        }
        if (tetrisInfo.targetLine && tetrisInfo.line >= tetrisInfo.targetLine) {
            // 存在目标行数 且已到达目标行数
            console.log("gameOver(true)")
            gameOver(true)
        }
        // 更新统计
        Object.keys(clearLineInfo.tetrisTotal).map((key) => {
            tetrisTotal[key] = clearLineInfo.tetrisTotal[key]
        })
        tetrisInfo.lastClearLineRafTime = rafTimer.rafTime // 记录最后一次消行时间
    } else {
        // 未消行
        let lastRen = data.ren
        if (tetrisInfo.renLimitInfo.enable && lastRen >= tetrisInfo.renLimitInfo.limit && tetrisInfo.renLimitInfo.endTime == 0) {
            // 开启了连击限制 且 达到了限制次数 且未进入限制状态
            // 触发连击限制
            tetrisInfo.renLimitInfo.endTime = rafTimer.rafTime + tetrisInfo.renLimitInfo.duration
        }
        data.showAtkLinksCount = false
    }
    updateFixedCourtIndex() // 添加场地index
    if (skillText) showSkillText(skillText) // 显示特殊消行文本
    app.playSound(playAudioName) // 消行音效
    data.ren = ren
    if (ren && ren >= 1) app.playSound("r" + ren) // 连击音效
    if (keyBoard.sortDown) actionSortDownEnd() // 硬降后 如还处于软降中 终止软降
}

// 为当前所有固定块添加场地索引 固定块变更后执行 提高渲染性能
const updateFixedCourtIndex = () => {
    data.fixed = data.fixed.map((i: any) => {
        const index = data.tetrisList.findIndex((item: any) => item.x == i.x && item.y == i.y)
        if (index > -1) i.index = index
        return i
    })
}

// 左移动
const left = () => {
    let isOk = true
    // 继续左移则触底
    const isLeft = data.target.some((i: any) => {
        return i.x <= 0
    })
    // 继续左移则与固定块重合
    const isNearFixed = data.target.some((i: any) => {
        return data.fixed.some((item: any) => {
            return item.x == i.x - 1 && item.y == i.y
        })
    })
    isOk = !isLeft && !isNearFixed
    let audioName = "movefail"
    // 可以移动
    if (isOk) {
        data.isCanFreeFall = false
        data.target = data.target.map((i: any) => {
            i.x--
            return i
        })
        if (data.type != "single")
            app.updateTargetBlock({
                roomId: app.roomId,
                list: data.target,
            })
        renderShadow()
        data.isCanFreeFall = true
        audioName = "move"
        data.lastAction = "move"
    }
    // app.playSound(audioName) // 移动音效
}

// 右移动
const right = () => {
    let isOk = true
    // 继续右移则触底
    const isRight = data.target.some((i: any) => {
        return i.x >= 9
    })
    // 继续右移则与固定块重合
    const isNearFixed = data.target.some((i: any) => {
        return data.fixed.some((item: any) => {
            return item.x == i.x + 1 && item.y == i.y
        })
    })
    isOk = !isRight && !isNearFixed
    let audioName = "movefail"
    // 可以移动
    if (isOk) {
        data.isCanFreeFall = false
        data.target = data.target.map((i: any) => {
            i.x++
            return i
        })
        if (data.type != "single")
            app.updateTargetBlock({
                roomId: app.roomId,
                list: data.target,
            })
        renderShadow()
        data.isCanFreeFall = true
        audioName = "move"
        data.lastAction = "move"
    }
    // app.playSound(audioName) // 移动音效
}

// 旋转
const spin = () => {
    data.isCanFreeFall = false
    data.deg = data.deg + 90 >= 360 ? 0 : data.deg + 90
    const _target = JSON.parse(JSON.stringify(data.target))
    const _fixed = JSON.parse(JSON.stringify(data.fixed))
    const afterTarget = getAfterSpinBlock(_target, _fixed, data.deg)
    if (!afterTarget) {
        data.deg = data.deg - 90 < 0 ? 270 : data.deg - 90
        app.playSound("rotatefail") // 播放音频
        return // 旋转失败
    }
    app.playSound("rotate") // 播放音频
    data.target = afterTarget
    if (data.type != "single")
        app.updateTargetBlock({
            roomId: app.roomId,
            list: data.target,
        })
    renderShadow()
    data.isCanFreeFall = true
    data.lastAction = "spin"
}

// 逆时针旋转
const spinBack = () => {
    data.isCanFreeFall = false
    data.deg = data.deg - 90 < 0 ? 270 : data.deg - 90
    const _target = JSON.parse(JSON.stringify(data.target))
    const _fixed = JSON.parse(JSON.stringify(data.fixed))
    const afterTarget = getAfterSpinBlock(_target, _fixed, data.deg, true)
    if (!afterTarget) {
        data.deg = data.deg + 90 >= 360 ? 0 : data.deg + 90
        app.playSound("rotatefail") // 播放音频
        return // 旋转失败
    }
    app.playSound("rotate") // 播放音频
    data.target = afterTarget
    if (data.type != "single")
        app.updateTargetBlock({
            roomId: app.roomId,
            list: data.target,
        })
    renderShadow()
    data.isCanFreeFall = true
    data.lastAction = "spin"
}

// hold
const hold = () => {
    if (tetrisInfo.canHold) {
        if (sortDownTimer) sortDownTimerStop() // 正在软降 停止软降
        data.isCanFreeFall = false
        data.bagList.hold()
        createNewBlock()
        renderShadow() // 渲染阴影
        data.isCanFreeFall = true
        app.playSound("hold") // hold音效
    } else {
        app.playSound("holdFail") // hold失败音效
    }
}

// 显示暂停弹窗
const handleShowPause = () => {
    if (data.status == 1) {
        timeStop()
    }
    data.status = -1
    pauseDialogEl.value.show()
    app.initKeyBoard("pause") // 切换键盘操作mode
}

// 关闭暂停弹窗
const handleHidePause = () => {
    if (data.status == -1) {
        data.status = 1
        timeStart()
        app.initKeyBoard("game") // 切换键盘操作mode
    }
}

// 显示设置弹窗
const handleShowSetting = () => {
    data.showSetting = true
    SettingDialog.value.open()
}

// 关闭设置弹窗
const settingHide = (isChange = false) => {
    if (data.status == -1) {
        if (isChange) {
            initTetrisConfig()
        }
        data.status = 1
        timeStart()
        app.initKeyBoard("game") // 切换键盘操作mode
    }
}

// 特殊消行后 显示特殊消行说明
const showSkillText = (skillText: any) => {
    data.skillText = skillText
    setTimeout(() => {
        data.skillText = ""
    }, 1000)
}

// 发起攻击
const sendAtk = (num: number) => {
    if (data.atkLoader.hasAtk) {
        // 存在累计垃圾行
        num = data.atkLoader.counteract(num) // 进行抵消
    }
    if (num) {
        if (data.type == "single") {
            console.log("发起攻击", num)
            if (tetrisInfo.confrontModeInfo.enable) {
                // 当前为单人 - 对抗模式
                tetrisInfo.confrontModeInfo.hp -= num
                if (tetrisInfo.confrontModeInfo.hp <= 0) gameOver(true)
            }
        } else
            app.gameSendAtk({
                num,
                roomId: app.roomId,
            })

        // 处理短期爆发
        data.atkLinksCount += num
        data.showAtkLinksCount = true
    }
    if (num >= 4 && data.skillText) {
        // 特殊消行(非连击)造成4行以上伤害
        tetrisTranslateY.value = tetrisTranslateY.value + 1
        tetrisOpacity.value = 0.9
        resetTetrisPlayerTransform()
    }
}

// 受到攻击
const getAtk = (num: number) => {
    console.log("getAtk", num)
    if (data.status == 1 && !data.isGameOver) {
        data.atkLoader.addAtk(num)
        app.playSound("dangerus")
        // 更新统计
        tetrisTotal.getAtkTotal += num
        if (data.type == "single" && app.dgInfo.enable && (app.dgInfo.damageStrength[0] || app.dgInfo.damageStrength[1])) {
            // 单人模式 且 开启了dg-lab 且 开启了受到攻击后电击
            app.dgInfo.damageOn = true
            app.clearWave() // 清除当前播放的波形
            let hasChangeChannelA = false
            let hasChangeChannelB = false
            let strengthA = 0
            let strengthB = 0
            if (tetrisDgInfo.damageStrength[0]) {
                if (app.dgInfo.strength[0] != app.dgInfo.damageStrength[0]) {
                    strengthA = tetrisDgInfo.strength[0] // 保存修改前的常驻强度
                    app.dgSetStrength(1, tetrisDgInfo.damageStrength[0]) // 修改强度
                    hasChangeChannelA = true
                }
                app.dgSendWave(1, 1) // a通道 持续1秒
            }
            if (tetrisDgInfo.damageStrength[1]) {
                if (app.dgInfo.strength[1] != app.dgInfo.damageStrength[1]) {
                    strengthB = tetrisDgInfo.strength[1]
                    app.dgSetStrength(2, app.dgInfo.damageStrength[1])
                    hasChangeChannelB = true
                }
                app.dgSendWave(2, 1) // b通道 持续1秒
            }
            if (dgReloadTimer) {
                clearTimeout(dgReloadTimer)
                dgReloadTimer = null
            }
            dgReloadTimer = setTimeout(() => {
                // 恢复强度
                if (hasChangeChannelA) app.dgSetStrength(1, strengthA)
                if (hasChangeChannelB) app.dgSetStrength(2, strengthB)
                app.dgInfo.damageOn = false
            }, 1000)
        }
    }
}

// --------------------------------- 攻击相关 开始 -----------------------------------

// 初始化攻击加载器
const initAtkLoader = () => {
    let mode = tetrisInfo.createRubbishInfo.createTimeMode
    data.atkLoader.init({
        mode,
        LoadTime: tetrisInfo.createRubbishInfo.delayInfo.delay,
        MaxLoadNumber: mode == "now" ? 0 : tetrisInfo.createRubbishInfo.delayInfo.maxCount, // 延迟垃圾行 一次最多受到的攻击
        LoadEndCallback: () => {
            // 延迟垃圾行 垃圾行加载完毕回调
            app.playSound("dangerus1")
        },
    })
    if (data.type != "single") {
        data.atkLoader.changeCb = () => {
            app.updateAtkLine({
                roomId: app.roomId,
                num: atkLineHeight.value,
            })
        }
    }
}

// --------------------------------- 攻击相关 结束 -----------------------------------

// ---------------------------------- 计时器 ----------------------------------
// das计时器
const dasTimerStart = (type = "left") => {
    clearInterval(dasTimer)
    dasTimer = null
    dasTimer = setInterval(() => {
        if (type == "left") left()
        else right()
    }, data.arr)
}
const dasTimerStop = () => {
    clearInterval(dasTimer)
    dasTimer = null
}
// 软降计时器
const sortDownTimerStart = () => {
    data.isCanFreeFall = false // 停止正常下落
    clearInterval(sortDownTimer)
    sortDownTimer = null
    sortDownTimer = setInterval(() => {
        down(true)
    }, data.sortArr)
}
const sortDownTimerStop = () => {
    data.isCanFreeFall = true // 恢复正常下落
    clearInterval(sortDownTimer)
    sortDownTimer = null
}
// 初始化对局计时器
const initTetrisTimer = () => {
    // 重力下落配置
    let nextDownTime = 0
    let interval = 0
    if (tetrisInfo.gravity) {
        interval = 17 / tetrisInfo.gravity // 每次自由下落的间隔 = 帧 / 每次下落一格所需的帧数的倒数
        nextDownTime = interval
    }
    // 创建垃圾行行配置
    const createRubbishInfo = tetrisInfo.createRubbishInfo
    const acrInfo = tetrisInfo.autoCreateRubbishInfo
    let createRubbishInterval = 0
    if (createRubbishInfo.enable) createRubbishInterval = createRubbishInfo.frequency * 1000 // 生成垃圾行的间隔时间
    let nextCreateRubbishTime = createRubbishInterval

    let nextAutoCreateRubbishTime = 0
    if (acrInfo.enable) nextAutoCreateRubbishTime = acrInfo.delay // 挖掘模式专用 自动生成垃圾行且无视atkLoader

    // 对抗模式配置
    let nextRecoverTime = 0
    // 短期爆发重置时间
    let nextAtkLinksResetTime = 0
    // 下次触发常驻电击的时间
    let nextShockTime = 0

    rafTimer.init() // 初始化raf定时器
    rafTimer.setCb((time: number) => {
        tetrisInfo.time = time
        if (tetrisInfo.gravity && nextDownTime <= time) {
            // 存在重力 下落
            down()
            nextDownTime = time + interval
        }
        if (!data.showAtkLinksCount) {
            // 短期爆发统计进入重置阶段
            if (nextAtkLinksResetTime == 0) nextAtkLinksResetTime = time + 2000
            else if (nextAtkLinksResetTime && nextAtkLinksResetTime <= time) data.atkLinksCount = 0
        } else nextAtkLinksResetTime = 0
        if (createRubbishInterval && nextCreateRubbishTime <= time) {
            getAtk(createRubbishInfo.count) // 生成垃圾行
            nextCreateRubbishTime = time + createRubbishInterval
        }
        if (tetrisInfo.renLimitInfo.endTime && tetrisInfo.renLimitInfo.endTime < time) {
            // 当前已进入连击限制状态 且已过限制时间
            tetrisInfo.renLimitInfo.endTime = 0 // 重置状态
        }
        if (acrInfo.enable) {
            // 开启自动生成垃圾行(每隔一段时间自动生成垃圾行 且不会经过atkLoader直接生成)
            if (nextAutoCreateRubbishTime < time) {
                nextAutoCreateRubbishTime = nextAutoCreateRubbishTime + acrInfo.delay
                const createRubbishLineResult = autoCreateRubbishLine(data.target, data.fixed, acrInfo.count, acrInfo.airSpan)
                data.fixed = createRubbishLineResult.resultFixed // 创建垃圾行后的固定块
                data.target = createRubbishLineResult.resultTarget // 创建垃圾行后的当前块
                updateFixedCourtIndex() // 更新场地index
                renderList() // 渲染
                let save = !createRubbishLineResult.gameover
                if (save) renderShadow() // 渲染阴影
                else gameOver()
            }
        }
        // 当前为对抗模式
        if (tetrisInfo.confrontModeInfo.enable) {
            if (tetrisInfo.confrontModeInfo.hp < tetrisInfo.confrontModeInfo.maxHp) {
                // hp非满
                if (nextRecoverTime == 0) {
                    nextRecoverTime = time + tetrisInfo.confrontModeInfo.hpRecoverSpeed // 进入恢复状态
                    console.log("进入恢复状态 time hp 下次恢复血量时间", time, tetrisInfo.confrontModeInfo.hp, nextRecoverTime)
                } else if (nextRecoverTime <= time) {
                    tetrisInfo.confrontModeInfo.hp++ // 恢复血量
                    nextRecoverTime = time + tetrisInfo.confrontModeInfo.hpRecoverSpeed // 下次恢复血量时间
                    console.log("回血 time hp 下次恢复血量时间", time, tetrisInfo.confrontModeInfo.hp, nextRecoverTime)
                }
            } else nextRecoverTime = 0 // 血量回满 退出血量恢复状态
        }
        // 已开启dg-lab 且常驻强度大于0
        if (tetrisDgInfo.enable && (tetrisDgInfo.strength[0] > 0 || tetrisDgInfo.strength[1] > 0)) {
            if (!app.dgInfo.gameOn && !app.dgInfo.damageOn) {
                // 常驻电击开关关闭 且 未处于受伤电击状态 开启常驻电击
                if (nextShockTime <= time || time == 0) {
                    app.dgInfo.gameOn = true // 开始常驻电击
                    nextShockTime = time + 2000
                    if (tetrisDgInfo.strength[0]) app.dgSendWave(1, 2) // a通道 持续2秒
                    if (tetrisDgInfo.strength[1]) app.dgSendWave(2, 2) // b通道 持续2秒
                }
            }
            if (app.dgInfo.gameOn && nextShockTime <= time) {
                // 常驻电击已开启 且已超时
                app.dgInfo.gameOn = false
            }
        }
    })
}
// 开始计时
const timeStart = () => {
    rafTimer.start()
}
const timeStop = () => {
    rafTimer.pause()
}

// ---------------------------------- 计时器结束 ----------------------------------

// ---------------------------------- 键盘操作 ----------------------------------
let dasStartTimer = null as any
const actionLeft = () => {
    if (keyBoard.left) return
    keyBoard.left = true
    keyBoard.right = false
    left()
    clearTimeout(dasStartTimer)
    dasStartTimer = null
    dasStartTimer = setTimeout(() => {
        dasTimerStart("left")
    }, data.das)
}

const actionLeftEnd = () => {
    keyBoard.left = false
    if (keyBoard.right) return // 继承das
    clearTimeout(dasStartTimer)
    dasStartTimer = null
    dasTimerStop()
}

const actionRight = () => {
    if (keyBoard.right) return
    keyBoard.right = true
    keyBoard.left = false
    right()
    clearTimeout(dasStartTimer)
    dasStartTimer = null
    dasStartTimer = setTimeout(() => {
        dasTimerStart("right")
    }, data.das)
}

const actionRightEnd = () => {
    keyBoard.right = false
    if (keyBoard.left) return // 继承das
    clearTimeout(dasStartTimer)
    dasStartTimer = null
    dasTimerStop()
}

const actionSortDown = () => {
    if (keyBoard.sortDown) return
    keyBoard.sortDown = true
    sortDownTimerStart() // 开始软降
}

const actionSortDownEnd = () => {
    keyBoard.sortDown = false
    sortDownTimerStop() // 软降关闭
    timeStart() // 正常下落开启
}

const actionHardDown = () => {
    if (keyBoard.hardDown) return
    keyBoard.hardDown = true
    hardDown()
}

const actionHardDownEnd = () => {
    keyBoard.hardDown = false
}

const actionSpin = () => {
    if (keyBoard.spin) return
    keyBoard.spin = true
    spin()
}

const actionSpinEnd = () => {
    keyBoard.spin = false
}

const actionSpinBack = () => {
    if (keyBoard.spinBack) return
    keyBoard.spinBack = true
    spinBack()
}

const actionSpinBackEnd = () => {
    keyBoard.spinBack = false
}

const actionHold = (e: any) => {
    if (keyBoard.hold) return
    keyBoard.hold = true
    // 阻止默认行为
    if (e) e.preventDefault()
    hold()
}

const actionHoldEnd = () => {
    keyBoard.hold = false
}

const actionPause = (e: any = false) => {
    // 阻止默认行为
    e && e.preventDefault()
    if (data.type != "single") return // 非单人模式 禁止暂停
    if (data.status == -1) {
        pauseDialogEl.value.hide() // 已暂停 -> 继续
    } else handleShowPause()
}

// ---------------------------------- 键盘操作结束 ----------------------------------

// ---------------------------------- 初始化 ----------------------------------

let readyTimer = null as any // 预备定时器

const initGame = () => {
    app.initKeyBoard("") // 重置键盘操作
    initTetrisSize()
    initList()
    initTetrisData()
    initTetrisInfo()
    initTetrisTotal()
    initTetrisConfig()
    initUserConfig()
    initTetrisTimer() // 初始化定时器
    initAtkLoader() // 初始化垃圾行加载器
    data.bagList.init(tetrisInfo) // 初始化方块袋
    if (readyTimer) {
        // 初始化预备定时器
        clearInterval(readyTimer)
        readyTimer = null
    }
    // 当前模式存在方块锁定延迟 初始化锁定延迟计时器
    if (tetrisInfo.lockDelay > 0) {
        data.lockDelayTimer.init(tetrisInfo.lockDelay)
    }
}

// 初始化游戏后 渲染默认地图信息
const initDefaultTetrisCourt = () => {
    let fixed = initTetrisCourt(tetrisInfo.defaultTetrisCourt, tetrisInfo.defaultTetrisCourtLevel)
    // console.log('fixed',fixed)
    data.fixed = fixed
    updateFixedCourtIndex() // 更新场地index
    renderList() // 渲染
}

const gameReady = () => {
    if (readyTimer) {
        // 初始化预备定时器
        clearInterval(readyTimer)
        readyTimer = null
    }
    return new Promise((resolve) => {
        data.status = 2 // 状态变更: 准备中
        data.readyCount = 6
        if (!data.firstGame) data.readyCount = 3 // 非首局游戏 预备期间只显示倒计时
        readyTimer = setInterval(() => {
            data.readyCount--
            if (data.readyCount < 0) {
                clearInterval(readyTimer)
                readyTimer = null
                resolve()
            }
        }, 1000)
    })
}

const gameStart = () => {
    data.status = 1
    app.initKeyBoard("game")
    // 播放对战音乐
    app.playBgm("battle")
    const currentSong = app.audioBase.targetBgmName
    // 如果返回的歌曲名不是默认歌曲，则显示
    if (currentSong && currentSong !== "singleplay01") {
        songName.value = currentSong
        showSongName.value = true
        setTimeout(() => {
            showSongName.value = false
        }, 3000)
    }
    data.bagList.next() // 方块袋初始化后 方块targetIndex为-1 需先调用next 再创建第一个方块
    createNewBlock() // 创建第一个方块
    renderList() // 渲染
    renderShadow() // 渲染阴影
    timeStart() // 开始计时
    app.initKeyBoard("game") // 切换键盘操作mode
}

const restart = () => {
    if (gsapGameOverCtx != null) gsapGameOverCtx.revert() // 重置元素状态
    initGame()
    initTetrisAnimateStatus()
    if (tetrisInfo.defaultTetrisCourt) initDefaultTetrisCourt()
    if (data.type != "single") {
        // 非单人模式 推送初始化的对手块序
        app.updatePlayerInfo({
            roomId: app.roomId,
            nextMinoList: data.bagList.nextMinoList,
            hold: data.bagList.targetHold,
        })
    }
    gameReady().then(() => {
        gameStart()
        firework.init(canvasEl.value)
        tetrisInfo.start = dayjs().valueOf()
    })
}

const again = () => {
    if (data.type == "single") {
        data.firstGame = false
        restart()
    } else {
        sessionStorage.setItem("quickMatchOn", "on") // 回到首页 自动加入匹配
        router.replace({
            path: "/",
        })
    }
}

const handleCourtBlockAnimate = () => {
    // 游戏结束 使用gsap动画
    const base = gsap.to(".courtItem", {
        duration: 1.6,
        physics2D: {
            velocity: "random(400, 1000)",
            angle: "random(250, 290)",
            gravity: 2000,
        },
        stagger: {
            // 每个元素之间的间隔
            grid: [20, 10], // 网格布局
            from: 0, // 从第几个元素开始
            amount: 0.3, // 间隔时间
        },
    })
    base.play() // 游戏结束动画
    gsapGameOverCtx = base
}

const gameOver = (win = false) => {
    if (data.mode == "zen") {
        // 自由模式 死亡判定后继续游戏
        tetrisAnimateStatus.player = "gameover" // 场地下落
        timeStop() // 停止计时
        renderGameOverList() // 所有固定块渲染为垃圾行
        data.showWarning = false // 动画前把警告去掉
        handleCourtBlockAnimate() // 游戏结束 使用gsap动画
        app.initKeyBoard("") // 重置键盘操作
        setTimeout(() => {
            initList()
            data.fixed = []
            renderList() // 空场地渲染
            initTetrisAnimateStatus() // 重置场地位置到初始位置
            gsapGameOverCtx.reverse() // 反向播放动画
        }, 1600)
        setTimeout(() => {
            createNewBlock() // 创建第一个方块
            renderShadow() // 渲染阴影
            timeStart() // 开始计时
            app.initKeyBoard("game") // 切换键盘操作mode
        }, 3200)
        return
    }
    if (data.isGameOver) return // 防止在游戏结束动画中再次调用
    data.isGameOver = true
    tetrisInfo.end = dayjs().valueOf()
    const gameResult = gameResultTotal()
    timeStop() // 停止计时
    app.initKeyBoard("") // 重置键盘操作
    renderGameOverList() // 所有固定块渲染为垃圾行
    // renderVictoryList()
    app.stopBgm()
    if (app.dgInfo.enable) {
        // 已开启dg-lab 清除所有正在播放的波形
        app.clearWave()
    }
    if (win) {
        app.playSound("win-single")
        tetrisAnimateStatus.player = "win"
        tetrisAnimateStatus.opponent = "gameover"
    } else {
        app.playSound("gameover")
        tetrisAnimateStatus.opponent = "win"
        tetrisAnimateStatus.player = "gameover"
        handleCourtBlockAnimate() // 游戏结束 使用gsap动画
        if (data.type == "single" && app.dgInfo.enable && (app.dgInfo.gameoverStrength[0] || app.dgInfo.gameoverStrength[1])) {
            // 单人模式 且 开启了dg-lab 且 开启了游戏结束后电击
            let strength = [tetrisDgInfo.strength[0], tetrisDgInfo.strength[1]]
            app.dgSetStrength(1, app.dgInfo.gameoverStrength[0])
            app.dgSetStrength(2, app.dgInfo.gameoverStrength[1])
            app.dgSendWave(1, 1) // a通道 持续1秒
            app.dgSendWave(2, 1) // b通道 持续1秒
            if (dgReloadTimer) {
                clearTimeout(dgReloadTimer)
                dgReloadTimer = null
            }
            dgReloadTimer = setTimeout(() => {
                // 恢复强度
                app.dgInfo.strength[0] = strength[0]
                app.dgInfo.strength[1] = strength[1]
                app.dgSetStrength(1, strength[0])
                app.dgSetStrength(2, strength[1])
            }, 1000)
        }
    }
    if (data.type != "single" && !win) {
        // 联机对战 游戏失败后推送至房间其他成员
        app.updateTargetBlock({
            // 推送死亡前最后一个方块
            roomId: app.roomId,
            list: data.target,
        })
        app.gameOver({
            roomId: app.roomId,
        })
    }
    if (data.type == "solo" && data.mode == "quickMatch") {
        // 快速匹配单人对战 游戏结束后退出房间
        app.exitMatchRoom({
            // 退出快速匹配房间
            roomId: app.roomId,
        })
    }
    // 动画与音频结束后 重置
    setTimeout(() => {
        initGame()
        app.playBgm("lobby")
        gameResultPopupEl.value.show(gameResult, win)
    }, 2000)
}

const exit = () => {
    timeStop() // 停止计时
    app.initKeyBoard("") // 重置键盘操作
    router.replace({
        path: "/",
    })
}

// ---------------------------------- 初始化结束 ----------------------------------

onMounted(() => {})

onActivated(() => {
    let query = route.query
    data.mode = query.mode
    data.type = query.type || "single"
    data.firstGame = true
    app.loadKeyBoardAction({
        // 加载回调函数
        actionHardDown,
        actionLeft,
        actionRight,
        actionSortDown,
        actionSpin,
        actionSpinBack,
        actionHold,
        actionPause,
        actionLeftEnd,
        actionRightEnd,
        actionSortDownEnd,
        actionHardDownEnd,
        actionSpinEnd,
        actionSpinBackEnd,
        actionHoldEnd,
    })
    app.stopBgm()
    app.initKeyBoard("") // 重置键盘操作

    // 非单人模式 挂载推送事件
    if (data.type != "single") {
        console.log("当前为多人对战, 挂载接受推送事件 data.type", data.type)
        // 玩家场地更新(仅solo)
        app.injectIoCb("updateFixedList", updateOpponentFixedList)
        // 玩家当前块更新(仅solo)
        app.injectIoCb("updateTargetBlock", (e: any) => {
            data.opponentTarget = e.list
        })
        // 玩家游戏结束
        app.injectIoCb("opponentGameOver", (e: any) => {
            if (data.type == "solo") gameOver(true) // 游戏结束 / 游戏胜利
            else console.log(`当前为多人对战, 玩家 (id: ${e.targetUserId}) 游戏结束`)
        })
        // 玩家发起攻击
        app.injectIoCb("opponentSendAtk", (e: any) => {
            if (data.type == "solo") getAtk(e.num)
            else console.log(`当前为多人对战, 玩家 (id: ${e.targetUserId}) 发起攻击 攻击数量: ${e.num}`)
        })
        // 玩家受击条变化
        app.injectIoCb("opponentAtkLineChange", (e: any) => {
            if (data.type == "solo") data.opponentAtk = e.num
            else console.log(`当前为多人对战, 玩家 (id: ${e.targetUserId}) 受击条变化 受击条数量: ${e.num}`)
        })
        // 玩家信息更新
        app.injectIoCb("opponentUpdatePlayerInfo", (e: any) => {
            if (data.type == "solo") {
                data.opponentHold = e.info.hold || ""
                data.opponentNextList = e.info.nextMinoList || []
            } else console.log(`当前为多人对战, 玩家 (id: ${e.targetUserId}) 更新信息: ${JSON.stringify(e.info)}`)
        })
        // 玩家离开房间
        app.injectIoCb("playerLeaveRoom", (e: any) => {
            if (data.type == "solo") {
                gameOver(true) // 游戏结束 / 游戏胜利
            } else console.log(`当前为多人对战, 玩家 (id: ${e.targetUserId}) 离开房间`)
        })
    } else {
        // 单人模式
        // 加载dgInfo
        Object.keys(app.dgInfo).forEach((key) => {
            let val = app.dgInfo[key]
            tetrisDgInfo[key] = val
        })
    }

    restart()

    // 监听页面可见性
    document.addEventListener("visibilitychange", watchVisibilitychange)
})

onDeactivated(() => {
    app.initKeyBoard("") // 重置键盘操作
    setTimeout(() => {
        // 等待路由动画结束 再重置
        initGame()
    }, 500)
    // 取消监听
    document.removeEventListener("visibilitychange", watchVisibilitychange)
    if (gsapGameOverCtx != null) gsapGameOverCtx.revert() // 重置元素状态
})

const watchVisibilitychange = () => {
    let visibilityState = document.visibilityState // 页面可见性
    rafTimer.pageVisibilityStateChange(visibilityState == "hidden" ? false : true)
    // 页面不可见时 暂停raf计时器(raf计时器在页面不可见时不会执行) 替换为普通计时器
}
</script>

<style lang="scss" scoped>
// 变量
$border_b: rgba(255, 255, 255, 0.5);
$border: rgba(255, 255, 255, 0.1);
$bg: rgba(255, 255, 255, 0.2);

.tetris {
    border: 1px solid $border_b;
    border-top: none;
    box-sizing: border-box;
    display: flex;
    flex-wrap: wrap;
    position: relative;

    &.winAnimate {
        animation: winAnimate 1.5s forwards;
    }

    &.gameoverAnimate {
        animation: gameoverAnimate 1.5s forwards;
    }

    &.tetrisEnter {
        animation: tetrisEnter ease-in-out 1.2s forwards;
    }

    &.game {
        transform: v-bind("tetrisPlayerTransform");
        opacity: v-bind("tetrisOpacityOutPut");
    }

    &.showWarning {
        transition: all 1s;
        border: 1px solid transparent;
        border-top: 1px solid rgba(245, 16, 56, 0.2);
        box-shadow: 0 0 10vmin rgba(245, 16, 56, 0.5);
    }

    .item {
        border: 1px solid $border;
        border-left: none;
        border-top: none;
        position: relative;

        &.notRight {
            border-right: none;
        }

        &.notBottom {
            border-bottom: none;
        }

        &.top {
            opacity: 0.8 !important;
        }

        &.showWarning {
            // 前置滤镜
            &::after {
                content: "";
                position: absolute;
                top: 0;
                left: 0;
                width: 100%;
                height: 100%;
                background: rgba(254, 18, 57, 0.05);
            }
        }

        &.hasColor {
            opacity: 1;
            border-radius: 16%;
        }

        &.shadow {
            opacity: 0.2 !important;
        }

        &.targetItem {
            z-index: 9;
            position: absolute;
        }

        &.animate_grind {
            animation: grind 0.8s;
        }

        .lightBorder {
            width: 100%;
            height: 100%;
            border: 1px solid #5865f2;
            border-radius: 16%;
            box-shadow: 0 0 1vmin rgb(88, 101, 242, 0.2);
            opacity: 0;
            animation: show 0.8s 1 forwards;
        }

        @keyframes show {
            0% {
                opacity: 0;
            }

            100% {
                opacity: 1;
            }
        }
    }

    .court {
        &.hasColor {
            box-shadow: -2px -2px 8px rgba(0, 0, 0, 0.05), -4px -4px 16px rgba(100, 100, 100, 0.05);
        }
    }

    .tetrisCanvas {
        pointer-events: none;
    }

    @import url("https://fonts.googleapis.com/css?family=Sarpanch:900");

    $rotationAmt: -12;

    @keyframes wave {
        0% {
            transform: skew(#{$rotationAmt}deg) rotate(#{$rotationAmt}deg) translate(100%, 0) scale(1);
            opacity: 1;
        }

        10% {
            opacity: 0;
        }

        20% {
            opacity: 1;
        }

        30% {
            opacity: 0;
        }

        50% {
            opacity: 1;
        }

        100% {
            transform: skew(#{$rotationAmt - 1}deg) rotate(#{$rotationAmt + 1}deg) translate(100%, 0) scale(1.05);
            opacity: 1;
        }
    }

    .skillText {
        position: absolute;
        right: -4vmin;
        bottom: -4vmin;
        transform-style: preserve-3d;
        transform: skew(#{$rotationAmt}deg) rotate(#{$rotationAmt}deg) translate(100%, 0);
        grid-area: text;
        font-family: "Sarpanch", sans-serif;
        font-size: 10vmin;
        width: 20vmin;
        margin: 0;
        color: #1d9099;
        text-shadow: 1vmin 1vmin 0 #e79c10, -1vmin -1vmin 0 #d53a33;
        animation: wave 0.5s ease-in-out infinite alternate;
    }

    .hold {
        position: absolute;
        left: -2vmin;
        top: 2vmin;
        transform: translate(-100%, 0);
    }

    .renInfo {
        position: absolute;
        left: -2vmin;
        top: 20vmin;
        transform: translate(-100%, 0);
        font-family: "Floane";
        letter-spacing: 0.4vmin;
        font-size: 5vmin;
        color: #ccc;

        .num {
            font-size: 8vmin;
            color: #fff;
        }
    }

    .viewList {
        position: absolute;
        right: -4vmin;
        top: 0;
        transform: translate(100%, 0);

        .viewListItem {
            margin: 0 0 5vmin 4vmin;
        }
    }

    .atkBorder {
        width: 100%;
        height: 100%;
        position: absolute;
        left: 0;
        top: 0;
        animation: atkBorder 1s infinite alternate;
        opacity: 0.8;

        .bottom {
            position: absolute;
            left: 0;
            bottom: -1px;
            width: 100%;
            height: 0.5vmin;
            background: rgba($color: red, $alpha: 0.8);
            box-shadow: 0 0 5vmin red;
        }

        .left {
            position: absolute;
            left: -1px;
            bottom: 0;
            width: 0.5vmin;
            min-height: 0%;
            background: rgba($color: red, $alpha: 0.8);
            transition: all 0.5s;
            box-shadow: 0 0 5vmin red;
        }

        .right {
            position: absolute;
            right: -1px;
            bottom: 0;
            width: 0.5vmin;
            min-height: 0%;
            background: rgba($color: red, $alpha: 0.8);
            transition: all 0.5s;
            box-shadow: 0 0 5vmin red;
        }
    }

    .atkLine {
        position: absolute;
        height: 100%;
        width: 3vmin;
        bottom: 0;
        right: -1px;
        transform: translate(100%, 0);

        .line {
            position: absolute;
            height: 0;
            min-height: 0;
            width: 100%;
            bottom: 0;
            left: 0;
            transition: all 0.5s;
            background: linear-gradient(to top, rgb(69, 58, 148, 0.5), rgb(69, 58, 148), rgb(244, 59, 71, 0.5), rgb(244, 59, 71), rgb(69, 58, 148, 0.5));
            background-size: 100% 100vh;
            animation: loader 30s linear infinite;
            border-radius: 0.5vmin 0.5vmin 0 0;
            z-index: 2;
        }

        .loadEndLine {
            position: absolute;
            height: 0;
            min-height: 0;
            width: 100%;
            bottom: 0;
            left: 0;
            transition: all 0.5s;
            background: rgb(152, 9, 19);
            background-size: 100% 100vh;
            border-radius: 0.5vmin 0.5vmin 0 0;
            z-index: 1;
            animation: loader1 0.4s linear infinite alternate;
        }

        .line::before {
            content: "";
            position: absolute;
            inset: 0;
            background: linear-gradient(to top, rgb(69, 58, 148, 0.5), rgb(69, 58, 148), rgb(244, 59, 71, 0.5), rgb(244, 59, 71), rgb(69, 58, 148, 0.5));
            background-size: 100% 100vh;
            filter: blur(1vmin);
            animation: loader 30s linear infinite;
            border-radius: 0.5vmin 0.5vmin 0 0;
        }

        @keyframes loader {
            0% {
                background-position: 100% 100vh;
            }

            100% {
                background-position: 100% 600vh;
            }
        }

        @keyframes loader1 {
            0% {
                // 滤镜 明度 对比度
                filter: brightness(1) contrast(1);
            }

            100% {
                filter: brightness(1.5) contrast(1.5);
            }
        }
    }

    .tetrisInfo {
        position: absolute;
        left: -2vmin;
        bottom: 0;
        transform: translate(-100%, 0);
        font-family: "Regular";
        letter-spacing: 0.3vmin;

        .title {
            color: #ccc;
            font-size: 4vmin;
        }

        .content {
            color: #fff;
            font-size: 5vmin;
        }

        div {
            margin-bottom: 1.2vmin;
        }
    }

    .lockDelayLine {
        position: absolute;
        left: 50%;
        bottom: -3vmin;
        transform: translate(-50%, 0);
        background-image: linear-gradient(to right, #453a94 0%, #f43b47 50%, #453a94 100%);
        box-shadow: 0 0 5vmin #453a94;
        width: 0;
        height: 1.6vmin;
        border-radius: 0.8vmin;
        animation: shake 0.3s alternate infinite;
    }

    // 轻微抖动
    @keyframes shake {
        from,
        to,
        50% {
            transform: translate(-50%, 0);
            opacity: 1;
        }

        25% {
            transform: translate(calc(-50% + 0.1vmin), -0.2vmin);
            opacity: 0.9;
        }

        75% {
            transform: translate(calc(-50% - 0.1vmin), 0.2vmin);
            opacity: 0.9;
        }
    }

    .linksText {
        width: max-content;
        position: absolute;
        left: 50%;
        bottom: -7vmin;
        transform: translate(-50%, 0);
        font-family: "Sarpanch", sans-serif;
        font-size: 4vmin;
        color: #fff;
        text-shadow: 0.2vmin 0.2vmin 0 #e79c10, -0.2vmin -0.2vmin 0 #d53a33;
        opacity: 0;
        animation: linksShow 0.3s ease-in-out forwards;
        letter-spacing: 0.2vmin;
    }

    @keyframes linksShow {
        from {
            opacity: 0;
            transform: translate(-50%, 2vmin);
            letter-spacing: 0.2vmin;
        }

        to {
            opacity: 1;
            transform: translate(-50%, 0);
            letter-spacing: 1vmin;
        }
    }
}

.tetrisTop {
    position: fixed;
    top: 0;
    left: 0;
    z-index: 9999;
    width: 100%;
    height: 10vmin;
    box-shadow: 0 1vmin 10vmin rgba($color: #fff, $alpha: 0.1);
    background: rgba(0, 0, 0, 0.2);
    font-size: 4vmin;
    padding: 0 20vmin;

    .title {
        font-size: 3vmin;
        color: #eee;
        padding: 0 8vmin 0 3vmin;
        height: 100%;
        font-weight: 200;
        position: absolute;
        left: 0;
    }

    .vs {
        position: absolute;
        left: 50%;
        transform: translateX(-50%);
        font-size: 5vmin;
        font-weight: bolder;
        animation: vsShift 2s ease-in-out infinite alternate;
        background-image: -webkit-linear-gradient(180deg, #f7113b, #2ca2fa);
        -webkit-background-clip: text;
        -webkit-text-fill-color: transparent;
    }

    .name {
        flex: 1;
        position: relative;
        font-weight: 600;
        background-image: -webkit-linear-gradient(200deg, #08204b, #2ca2fa, #fff);
        -webkit-background-clip: text;
        -webkit-text-fill-color: transparent;

        &.opponentName {
            background-image: -webkit-linear-gradient(200deg, #000, #8f041d, #f7113b);
        }

        &::after {
            content: "";
            position: absolute;
            left: 50%;
            bottom: -2vmin;
            transform: translateX(-50%);
            height: 1px;
            width: 16vw;
            background: rgb(238, 238, 238, 0.3);
        }
    }
}

.bouncing {
    animation: bounce 0.15s;
}

.virtualButtonBox {
    width: 100vw;
    height: 100vh;
    position: fixed;
    top: 0;
    left: 0;
    z-index: 0;
}

.tetrisReady {
    position: fixed;
    left: 50%;
    top: 50%;
    transform: translate(-50%, -50%);
    font-family: "Floane";
    letter-spacing: 1vmin;
    color: #ccc;
    text-align: center;
    width: max-content;
    font-size: 8vmin;

    .readyAnimate {
        font-size: 12vmin;
        animation: textShadow 1s ease-in-out infinite alternate;
    }

    // 预备遮罩
    &::after {
        content: "";
        position: absolute;
        left: 50%;
        top: 50%;
        transform: translate(-50%, -50%);

        width: 100vw;
        height: 100vh;
        z-index: 0;
        pointer-events: none;
        background: #000;
        opacity: 0.05;
    }

    span {
        z-index: 1;
        position: relative;
    }
}

.tetrisReadyTitle {
    position: fixed;
    width: 100%;
    height: 100%;
    left: 50%;
    top: 50%;
    transform: translate(-50%, -50%);
    letter-spacing: 0.2vmin;
    color: #ddd;
    font-size: 8vmin;
    animation: tetrisReadyTitle 2.5s 1 forwards;

    .text {
        position: relative;
        z-index: 99;
    }

    .top,
    .bottom {
        width: 200vw;
        height: 6vh;
        position: absolute;
        height: 0;
        min-height: 6vh;
        opacity: 0.3;
    }

    .top {
        left: -100vw;
        top: 50%;
        transform: translate(-50%, -100%);
        background: #e64b3c;
        animation: tetrisReadyTitleTop 2s ease-in-out 1 forwards;
    }

    .bottom {
        right: -100vw;
        top: 50%;
        transform: translate(50%, 0);
        background: #4b8ee6;
        animation: tetrisReadyTitleBottom 2s ease-in-out 1 forwards;
    }

    @keyframes tetrisReadyTitleTop {
        50% {
            transform: translate(0, -100%);
            min-height: 12vh;
            opacity: 0.9;
        }

        80% {
            min-height: 12vh;
            transform: translate(0, 0);
        }

        to {
            min-height: 0;
            background: #fff;
            opacity: 0.05;
        }
    }

    @keyframes tetrisReadyTitleBottom {
        50% {
            transform: translate(0, 0);
            min-height: 12vh;
            opacity: 0.9;
        }

        80% {
            min-height: 12vh;
            transform: translate(0, -100%);
        }

        to {
            min-height: 0;
            background: #fff;
            opacity: 0.05;
        }
    }

    .inset {
        width: 100vw;
        height: 0;
        min-height: 0;
        position: absolute;
        top: 50%;
        transform: translate(0, -50%);
        background: rgb(119, 119, 119, 0.8);
        animation: tetrisReadyTitleInset 2.5s ease-in-out 1 forwards;
        opacity: 0;
    }

    @keyframes tetrisReadyTitleInset {
        30% {
            min-height: 30vh;
            opacity: 1;
        }

        80% {
            min-height: 30vh;
            opacity: 0.8;
            background: #000;
        }

        to {
            min-height: 0;
            letter-spacing: 2vmin;
            opacity: 0;
        }
    }

    @keyframes tetrisReadyTitle {
        30% {
            letter-spacing: 0;
        }

        80% {
            opacity: 0.8;
            color: #fff;
        }

        to {
            letter-spacing: 5vmin;
            opacity: 0;
        }
    }
}

@keyframes bounce {
    25% {
        transform: translateY(0.5%);
        animation-timing-function: cubic-bezier(0, 0, 0.2, 1);
    }

    0%,
    100% {
        transform: translateY(0);
        animation-timing-function: cubic-bezier(0.8, 0, 1, 1);
    }
}

@keyframes winAnimate {
    from {
        opacity: 1;
        transform: scale(1);
    }

    to {
        opacity: 0;
        transform: scale(1.5);
    }
}

@keyframes gameoverAnimate {
    from {
        opacity: 1;
        -webkit-transform: translate3d(0, 0, 0);
        transform: translate3d(0, 0, 0);
    }

    10% {
        -webkit-transform: translate3d(-25%, 0, 0) rotate3d(0, 0, 1, -5deg);
        transform: translate3d(-25%, 0, 0) rotate3d(0, 0, 1, -5deg);
    }

    20% {
        -webkit-transform: translate3d(20%, 0, 0) rotate3d(0, 0, 1, 3deg);
        transform: translate3d(20%, 0, 0) rotate3d(0, 0, 1, 3deg);
    }

    30% {
        -webkit-transform: translate3d(-15%, 0, 0) rotate3d(0, 0, 1, -3deg);
        transform: translate3d(-15%, 0, 0) rotate3d(0, 0, 1, -3deg);
    }

    40% {
        -webkit-transform: translate3d(10%, 0, 0) rotate3d(0, 0, 1, 2deg);
        transform: translate3d(10%, 0, 0) rotate3d(0, 0, 1, 2deg);
    }

    50% {
        -webkit-transform: translate3d(-5%, 0, 0) rotate3d(0, 0, 1, -1deg);
        transform: translate3d(-5%, 0, 0) rotate3d(0, 0, 1, -1deg);
    }

    60% {
        opacity: 1;
        transform: translateY(0);
    }

    to {
        opacity: 0;
        transform: translateY(100%);
    }
}

@keyframes grind {
    0% {
        // 滤镜 明度 对比度
        filter: brightness(1) contrast(1);
    }

    50% {
        filter: brightness(1.2) contrast(1.2);
    }

    100% {
        filter: brightness(1) contrast(1);
    }
}

@keyframes textShadow {
    0% {
        transform: translate(-50%, -50%) translateY(0);
        text-shadow: 0 0 0 #0c2ffb, 0 0 0 #2cfcfd, 0 0 0 #fb203b, 0 0 0 #fefc4b;
    }

    20% {
        transform: translate(-50%, -50%) translateY(-0.5vmin);
        text-shadow: 0 0.125em 0 #0c2ffb, 0 0.25em 0 #2cfcfd, 0 -0.125em 0 #fb203b, 0 -0.25em 0 #fefc4b;
    }

    40% {
        transform: translate(-50%, -50%) translateY(0.25vmin);
        text-shadow: 0 -0.0625em 0 #0c2ffb, 0 -0.125em 0 #2cfcfd, 0 0.0625em 0 #fb203b, 0 0.125em 0 #fefc4b;
    }

    60% {
        transform: translate(-50%, -50%) translateY(-0.12vmin);
        text-shadow: 0 0.03125em 0 #0c2ffb, 0 0.0625em 0 #2cfcfd, 0 -0.03125em 0 #fb203b, 0 -0.0625em 0 #fefc4b;
    }

    80% {
        transform: translate(-50%, -50%) translateY(0);
        text-shadow: 0 0 0 #0c2ffb, 0 0 0 #2cfcfd, 0 0 0 #fb203b, 0 0 0 #fefc4b;
    }
}

@keyframes tetrisEnter {
    0% {
        opacity: 0;
        transform: scale(2) translateY(-100%);
    }

    100% {
        opacity: 1;
        transform: scale(1) translateY(0);
    }
}

@keyframes vsShift {
    0%,
    40%,
    44%,
    58%,
    61%,
    65%,
    69%,
    73%,
    100% {
        transform: skewX(0deg);
    }

    41% {
        transform: skewX(10deg);
    }

    42% {
        transform: skewX(-10deg);
    }

    59% {
        transform: skewX(40deg) skewY(10deg);
    }

    60% {
        transform: skewX(-40deg) skewY(-10deg);
    }
}

.songName {
    position: fixed;
    left: 2vmin;
    bottom: 2vmin;
    padding: 2vmin 4vmin;
    background: rgba(0, 0, 0, 0.6);
    color: #fff;
    border-radius: 1vmin;
    font-size: 2vmin;
    z-index: 100;
    animation-duration: 0.5s;
    text-shadow: 0 0 0.3vmin rgba(0, 157, 255, 0.5);
    box-shadow: 0 0.2vmin 0.5vmin rgba(0, 157, 255, 0.2), inset 0 0 0.3vmin rgba(0, 157, 255, 0.1);
}
</style>
